#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <X11/Xlib.h>

#define NILL                            INT_MIN
#define LOCKFILE                        "/var/local/dwmblocks/dwmblocks.pid"

#define LENGTH(X)                       (sizeof X / sizeof X[0])

#define DELIMITERLENGTH                 (sizeof delimiter)
#define STATUSLENGTH                    (LENGTH(blocks) * (CMDOUTLENGTH + DELIMITERLENGTH) + 1)

#include "config.h"

_Static_assert(INTERVALs >= 0, "INTERVALs must be greater than or equal to 0");
_Static_assert(INTERVALn >= 0 && INTERVALn <= 999999999, "INTERVALn must be between 0 and 999999999");

static void buttonhandler(int sig, siginfo_t *info, void *ucontext);
static void cleanup(void);
static void setupsignals(void);
static void sighandler(int sig, siginfo_t *si, void *ucontext);
static void statusloop(void);
static void termhandler(int sig);
static void updateblock(Block *block, int sigval);
static void updatestatus(void);
static void writepid(void);

static Block *dirtyblock;
static Display *dpy;
static sigset_t blocksigmask;

void
buttonhandler(int sig, siginfo_t *info, void *ucontext)
{
    sig = info->si_value.sival_int >> 8;
    for (Block *block = blocks; block->pathu; block++)
        if (block->signal == sig)
            switch (fork()) {
            case -1:
                perror("buttonhandler - fork");
                break;
            case 0:
            {
                char button[] = { '0' + (info->si_value.sival_int & 0xff), '\0' };
                char *arg[] = { block->pathc, button, NULL };

                setsid();
                execv(arg[0], arg);
                perror("buttonhandler - child - execv");
                _exit(127);
            }
            }
}

void
cleanup(void)
{
    unlink(LOCKFILE);
    XStoreName(dpy, DefaultRootWindow(dpy), "");
    XCloseDisplay(dpy);
}

void
setupsignals(void)
{
    struct sigaction sa;

    /* populate blocksigmask and check validity of signals */
    sigemptyset(&blocksigmask);
    sigaddset(&blocksigmask, SIGHUP);
    sigaddset(&blocksigmask, SIGINT);
    sigaddset(&blocksigmask, SIGTERM);
    for (Block *block = blocks; block->pathu; block++) {
        if (block->signal <= 0)
            continue;
        if (block->signal > SIGRTMAX - SIGRTMIN) {
            fprintf(stderr, "Error: SIGRTMIN + %d exceeds SIGRTMAX.\n", block->signal);
            unlink(LOCKFILE);
            XCloseDisplay(dpy);
            exit(2);
        }
        sigaddset(&blocksigmask, SIGRTMIN + block->signal);
    }

    /* setup signal handlers */
    /* to handle HUP, INT and TERM */
    sa.sa_flags = SA_RESTART;
    sigemptyset(&sa.sa_mask);
    sa.sa_handler = termhandler;
    sigaction(SIGHUP, &sa, NULL);
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);

    /* to ignore unused realtime signals */
    // sa.sa_flags = SA_RESTART;
    // sigemptyset(&sa.sa_mask);
    sa.sa_handler = SIG_IGN;
    for (int i = SIGRTMIN + 1; i <= SIGRTMAX; i++)
        sigaction(i, &sa, NULL);

    /* to prevent forked children from becoming zombies */
    sa.sa_flags = SA_NOCLDSTOP | SA_NOCLDWAIT | SA_RESTART;
    // sigemptyset(&sa.sa_mask);
    sa.sa_handler = SIG_IGN;
    sigaction(SIGCHLD, &sa, NULL);

    /* to handle signals generated by dwm on click events */
    sa.sa_flags = SA_RESTART | SA_SIGINFO;
    // sigemptyset(&sa.sa_mask);
    sa.sa_sigaction = buttonhandler;
    sigaction(SIGRTMIN, &sa, NULL);

    /* to handle update signals for individual blocks */
    sa.sa_flags = SA_NODEFER | SA_RESTART | SA_SIGINFO;
    sa.sa_mask = blocksigmask;
    sa.sa_sigaction = sighandler;
    for (Block *block = blocks; block->pathu; block++)
        if (block->signal > 0)
            sigaction(SIGRTMIN + block->signal, &sa, NULL);
}

void
sighandler(int sig, siginfo_t *info, void *ucontext)
{
    sig -= SIGRTMIN;
    for (Block *block = blocks; block->pathu; block++)
        if (block->signal == sig)
            updateblock(block, info->si_value.sival_int);
    updatestatus();
}

void
statusloop(void)
{
    int i;
    struct timespec t;

    sigprocmask(SIG_BLOCK, &blocksigmask, NULL);
    for (Block *block = blocks; block->pathu; block++)
        if (block->interval >= 0)
            updateblock(block, NILL);
    for (i = 1; ; i++) {
        updatestatus();
        sigprocmask(SIG_UNBLOCK, &blocksigmask, NULL);
        t.tv_sec = INTERVALs, t.tv_nsec = INTERVALn;
        while (nanosleep(&t, &t) == -1);
        sigprocmask(SIG_BLOCK, &blocksigmask, NULL);
        for (Block *block = blocks; block->pathu; block++)
            if (block->interval > 0 && i % block->interval == 0)
                updateblock(block, NILL);
    }
}

void
termhandler(int sig)
{
    cleanup();
    exit(0);
}

void
updateblock(Block *block, int sigval)
{
    int fd[2];

    if (pipe(fd) == -1) {
        perror("updateblock - pipe");
        cleanup();
        exit(1);
    }
    switch (fork()) {
    case -1:
        perror("updateblock - fork");
        close(fd[0]);
        close(fd[1]);
        cleanup();
        exit(1);
    case 0:
        close(fd[0]);
        if (fd[1] != STDOUT_FILENO) {
            if (dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO) {
                perror("updateblock - child - dup2");
                close(fd[1]);
                exit(1);
            }
            close(fd[1]);
        }
        if (sigval == NILL) {
            char *arg[] = { block->pathu, NULL };

            execv(arg[0], arg);
        } else {
            char buf[12];
            char *arg[] = { block->pathu, buf, NULL };

            snprintf(buf, sizeof buf, "%d", sigval);
            execv(arg[0], arg);
        }
        perror("updateblock - child - execv");
        _exit(127);
    default:
    {
        size_t trd = 0;
        ssize_t rd;

        close(fd[1]);
        do
            rd = read(fd[0], block->curtext + trd, CMDOUTLENGTH - trd);
        while (rd > 0 && (trd += rd) < CMDOUTLENGTH);
        if (rd == -1) {
            perror("updateblock - read");
            close(fd[0]);
            cleanup();
            exit(1);
        }
        close(fd[0]);

        block->curtext[trd] = '\0';
        if (memcmp(block->curtext, block->prvtext, trd + 1) != 0) {
            memcpy(block->prvtext, block->curtext, trd + 1);
            if (!dirtyblock || block < dirtyblock)
                dirtyblock = block;
        }
        if (trd == 0)
            block->length = 0;
        else {
            if (block->curtext[trd - 1] == '\n')
                trd--;
            if (block->pathc)
                block->curtext[trd++] = block->signal;
            memcpy(block->curtext + trd, delimiter, DELIMITERLENGTH);
            block->length = trd + DELIMITERLENGTH;
        }
    }
    }
}

void
updatestatus(void)
{
    static char statustext[STATUSLENGTH];
    char *s = statustext;
    Block *block;

    if (!dirtyblock)
        return;
    for (block = blocks; block < dirtyblock; block++)
        s += block->length;
    for (; block->pathu; block++) {
        memcpy(s, block->curtext, block->length);
        s += block->length;
    }
    s[s == statustext ? 0 : -DELIMITERLENGTH] = '\0';
    dirtyblock = NULL;

    XStoreName(dpy, DefaultRootWindow(dpy), statustext);
    XSync(dpy, False);
}

void
writepid(void)
{
    int fd;
    struct flock fl;

    if ((fd = open(LOCKFILE, O_RDWR | O_CREAT | O_CLOEXEC,
                   S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) {
        perror("writepid - open");
        exit(1);
    }
    fl.l_type = F_WRLCK;
    fl.l_whence = SEEK_SET;
    fl.l_start = 0;
    fl.l_len = 0;
    if (fcntl(fd, F_SETLK, &fl) == -1) {
        if (errno == EACCES || errno == EAGAIN) {
            fputs("Error: another instance of dwmblocks is already running.\n", stderr);
            exit(2);
        }
        perror("writepid - fcntl");
        close(fd);
        exit(1);
    }
    if (ftruncate(fd, 0) == -1) {
        perror("writepid - ftruncate");
        close(fd);
        exit(1);
    }
    if (dprintf(fd, "%ld", (long)getpid()) < 0) {
        perror("writepid - dprintf");
        close(fd);
        exit(1);
    }
}

int
main(int argc, char *argv[])
{
    int xfd;

    writepid();
    if (!(dpy = XOpenDisplay(NULL))) {
        fputs("Error: could not open display.\n", stderr);
        unlink(LOCKFILE);
        return 1;
    }
    xfd = ConnectionNumber(dpy);
    fcntl(xfd, F_SETFD, fcntl(xfd, F_GETFD) | FD_CLOEXEC);
    setupsignals();
    statusloop();
    cleanup();
    return 0;
}
